﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using System.Globalization;
using Tesla.Math;

namespace Tesla.Graphics {
    /// <summary>
    /// Defines the 2D region where the 3D rendering is projected onto.
    /// </summary>
    public struct Viewport {
        /// <summary>
        /// Top left X coordinate of viewport.
        /// </summary>
        public int X;

        /// <summary>
        /// Top left Y coordinate of viewport.
        /// </summary>
        public int Y;

        /// <summary>
        /// Width of viewport.
        /// </summary>
        public int Width;

        /// <summary>
        /// Height of viewport.
        /// </summary>
        public int Height;

        /// <summary>
        /// Minimum Z depth.
        /// </summary>
        public float MinDepth;

        /// <summary>
        /// Maximum Z depth.
        /// </summary>
        public float MaxDepth;

        /// <summary>
        /// Get or set the bounds of this viewport as a Rectangle.
        /// </summary>
        public Rectangle Bounds {
            get {
                Rectangle r;
                r.X = this.X;
                r.Y = this.Y;
                r.Width = this.Width;
                r.Height = this.Height;
                return r;
            }
            set {
                this.X = value.X;
                this.Y = value.Y;
                this.Width = value.Width;
                this.Height = value.Height;
            }
        }

        /// <summary>
        /// Get the aspect ratio of the viewport, width divided by height.
        /// </summary>
        public float AspectRatio {
            get {
                if(this.Height != 0 && this.Width != 0){
                    return (float) this.Width / (float) this.Height;
                }
                return 0.0f;
            }
        }

        /// <summary>
        /// Creates a new viewport with the supplied top left corner coordinates,
        /// width, and height.
        /// </summary>
        /// <param name="x">Top left X coordinate</param>
        /// <param name="y">Top left Y coordinate</param>
        /// <param name="width">Width</param>
        /// <param name="height">Height</param>
        /// <param name="minDepth">Minimum Z depth</param>
        /// <param name="maxDepth">Maximum Z depth</param>
        public Viewport(int x, int y, int width, int height, int minDepth, int maxDepth) {
            this.X = x;
            this.Y = y;
            this.Width = width;
            this.Height = height;
            this.MinDepth = minDepth;
            this.MaxDepth = maxDepth;
        }

        /// <summary>
        /// Creates a new viewport with the supplied top left corner coordinates,
        /// width, and height.
        /// </summary>
        /// <param name="x">Top left X coordinate</param>
        /// <param name="y">Top left Y coordinate</param>
        /// <param name="width">Width</param>
        /// <param name="height">Height</param>
        public Viewport(int x, int y, int width, int height) {
            this.X = x;
            this.Y = y;
            this.Width = width;
            this.Height = height;
            this.MinDepth = 0.0f;
            this.MaxDepth = 1.0f;
        }

        /// <summary>
        /// Creates a new viewport from the supplied rectangle bounds.
        /// </summary>
        /// <param name="bounds">Rectangle bounds</param>
        public Viewport(Rectangle bounds) {
            this.X = bounds.X;
            this.Y = bounds.Y;
            this.Width = bounds.Width;
            this.Height = bounds.Height;
            this.MinDepth = 0.0f;
            this.MaxDepth = 1.0f;
        }

        /// <summary>
        /// Projects a 3D Vector from object space to screen space.
        /// </summary>
        /// <param name="source">Vector to project</param>
        /// <param name="worldMatrix">The world matrix</param>
        /// <param name="viewMatrix">The view matrix</param>
        /// <param name="projMatrix">The projection matrix</param>
        /// <returns>The projected vector</returns>
        public Vector3 Project(Vector3 source, Matrix worldMatrix, Matrix viewMatrix, Matrix projMatrix) {
            Matrix wv;
            Matrix.Multiply(ref worldMatrix, ref viewMatrix, out wv);
            Matrix wvp;
            Matrix.Multiply(ref wv, ref projMatrix, out wvp);

            Vector3 v;
            Vector3.Transform(ref source, ref wvp, out v);

            float w = (source.X * wvp.M14) + (source.Y * wvp.M24) + (source.Z * wvp.M34) + wvp.M44;
            if(!MathHelper.NearEpsilon(w, 1.0f)) {
                Vector3.Divide(ref v, w, out v);
            }

            v.X = (((v.X + 1.0f) * .5f) * this.Width) + this.X;
            v.Y = (((-v.Y + 1.0f) * .5f) * this.Height) + this.Y;
            v.Z = (v.Z * (this.MaxDepth - this.MinDepth)) + this.MinDepth;
            return v;
        }

        /// <summary>
        /// Projects a 3D Vector from object space to screen space.
        /// </summary>
        /// <param name="source">Vector to project</param>
        /// <param name="worldMatrix">The world matrix</param>
        /// <param name="viewMatrix">The view matrix</param>
        /// <param name="projMatrix">The projection matrix</param>
        /// <param name="result">Vector3 to hold the result</param>
        public void Project(ref Vector3 source, ref Matrix worldMatrix, ref Matrix viewMatrix, ref Matrix projMatrix, out Vector3 result) {
            Matrix wv;
            Matrix.Multiply(ref worldMatrix, ref viewMatrix, out wv);
            Matrix wvp;
            Matrix.Multiply(ref wv, ref projMatrix, out wvp);

            Vector3.Transform(ref source, ref wvp, out result);

            float w = (source.X * wvp.M14) + (source.Y * wvp.M24) + (source.Z * wvp.M34) + wvp.M44;
            if(!MathHelper.NearEpsilon(w, 1.0f)) {
                Vector3.Divide(ref result, w, out result);
            }

            result.X = (((result.X + 1.0f) * .5f) * this.Width) + this.X;
            result.Y = (((-result.Y + 1.0f) * .5f) * this.Height) + this.Y;
            result.Z = (result.Z * (this.MaxDepth - this.MinDepth)) + this.MinDepth;
        }

        /// <summary>
        /// Converts a point in screen space to a point in world space.
        /// </summary>
        /// <param name="source">Vector to un-project</param>
        /// <param name="worldMatrix">The world matrix</param>
        /// <param name="viewMatrix">The view matrix</param>
        /// <param name="projMatrix">The projection matrix</param>
        /// <returns>The un-projected vector</returns>
        public Vector3 UnProject(Vector3 source, Matrix worldMatrix, Matrix viewMatrix, Matrix projMatrix) {
            Matrix wv;
            Matrix.Multiply(ref worldMatrix, ref viewMatrix, out wv);
            Matrix wvp;
            Matrix.Multiply(ref wv, ref projMatrix, out wvp);
            Matrix invWvp;
            Matrix.Invert(ref wvp, out invWvp);

            Vector3 temp;
            temp.X = (((source.X - this.X) / ((float) this.Width)) * 2.0f) - 1.0f;
            temp.Y = -((((source.Y - this.Y) / ((float) this.Height)) * 2.0f) - 1.0f);
            temp.Z = (source.Z - this.MinDepth) / (this.MaxDepth - this.MinDepth);

            Vector3 v;
            Vector3.Transform(ref temp, ref invWvp, out v);

            float w = (temp.X * invWvp.M14) + (temp.Y * invWvp.M24) + (temp.Z * invWvp.M34) + invWvp.M44;
            if(!MathHelper.NearEpsilon(w, 1.0f)) {
                Vector3.Divide(ref v, w, out v);
            }

            return v;
        }

        /// <summary>
        /// Converts a point in screen space to a point in world space.
        /// </summary>
        /// <param name="source">Vector to un-project</param>
        /// <param name="worldMatrix">The world matrix</param>
        /// <param name="viewMatrix">The view matrix</param>
        /// <param name="projMatrix">The projection matrix</param>
        /// <param name="result">Vector3 to hold the result</param>
        public void UnProject(ref Vector3 source, ref Matrix worldMatrix, ref Matrix viewMatrix, ref Matrix projMatrix, out Vector3 result) {
            Matrix wv;
            Matrix.Multiply(ref worldMatrix, ref viewMatrix, out wv);
            Matrix wvp;
            Matrix.Multiply(ref wv, ref projMatrix, out wvp);
            Matrix invWvp;
            Matrix.Invert(ref wvp, out invWvp);

            Vector3 temp;
            temp.X = (((source.X - this.X) / ((float) this.Width)) * 2.0f) - 1.0f;
            temp.Y = -((((source.Y - this.Y) / ((float) this.Height)) * 2.0f) - 1.0f);
            temp.Z = (source.Z - this.MinDepth) / (this.MaxDepth - this.MinDepth);

            Vector3.Transform(ref temp, ref invWvp, out result);

            float w = (temp.X * invWvp.M14) + (temp.Y * invWvp.M24) + (temp.Z * invWvp.M34) + invWvp.M44;
            if(!MathHelper.NearEpsilon(w, 1.0f)) {
                Vector3.Divide(ref result, w, out result);
            }
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString() {
            CultureInfo info = CultureInfo.CurrentCulture;
            return String.Format(info, "{{X:{0} Y:{1} Width:{2} Height:{3} MinDepth:{4} MaxDepth:{5}}}",
                new Object[] { this.X.ToString(info), this.Y.ToString(info), this.Width.ToString(info), this.Height.ToString(info), this.MinDepth.ToString(info), this.MaxDepth.ToString(info) });
        }
    }
}
